package de.sdc.mario.engine.sonar;

final class SpcEmu extends SpcCpu
{
        private static final class Timer
        {
                int time; // time of next event
                int prescaler;
                int period;
                int divider;
                int enabled;
                int counter;
        }
       
        static final int ramSize                = 0x10000;
        static final int ramPadSize             = 0x100;
       
        // header offsets
        static final int cpuStateOff    = 0x25;
        static final int ramOff                 = 0x100;
        static final int dspStateOff    = 0x10100;
       
        static final int romAddr                = 0xFFC0;
       
        // SMP registers
        static final int testReg                = 0x0;
        static final int controlReg             = 0x1;
        static final int dspaddrReg             = 0x2;
        static final int dspdataReg             = 0x3;
        static final int cpuio0Reg              = 0x4;
        static final int cpuio1Reg              = 0x5;
        static final int cpuio2Reg              = 0x6;
        static final int cpuio3Reg              = 0x7;
        static final int f8Reg                  = 0x8;
        static final int f9Reg                  = 0x9;
        static final int t0targetReg    = 0xA;
        static final int t1targetReg    = 0xB;
        static final int t2targetReg    = 0xC;
        static final int t0outReg               = 0xD;
        static final int t1outReg               = 0xE;
        static final int t2outReg               = 0xF;
       
        static final int romSize = 0x40;
        static final int timerCount = 3;
        static final int regCount = 0x10;

        int                             dspTime;
        int                             romEnabled;
        byte []                 spc;
        final byte []   rom    = new byte [romSize];
        final byte []   hiRam  = new byte [romSize];
        final byte []   ram    = new byte [ramSize + ramPadSize];
        final int []    regs   = new int [regCount];
        final int []    regsIn = new int [regCount];
        final SpcDsp    dsp    = new SpcDsp();
        final Timer []  timers = new Timer [timerCount];

        protected int setSampleRate_( int rate ) { return rate; }
       
        protected int loadFile_( byte [] in )
        {
                if ( !isHeader( in, "SNES-SPC700 Sound File Data" ) )
                        error( "Not an SPC file" );
               
                spc = in;
               
                // almost no SPC music rely on more than last two bytes of boot ROM
                java.util.Arrays.fill( rom, 0, romSize, (byte) 0 );
                rom [0x3E] = (byte) 0xFF;
                rom [0x3F] = (byte) 0xC0;
               
                // TODO: use SPC file's copy of ROM, if present?
               
                return 1;
        }
       
        // Runs timer to present. Time must be >= t.time.
        static void runTimer_( Timer t, int time )
        {
                int elapsed = ((time - t.time) >> t.prescaler) + 1;
                t.time += elapsed << t.prescaler;
               
                if ( t.enabled != 0 )
                {
                        int remain = ((t.period - t.divider - 1) & 0xFF) + 1;
                        int divider = t.divider + elapsed;
                        int over;
                        if ( (over = elapsed - remain) >= 0 )
                        {
                                int n = over / t.period;
                                t.counter = (t.counter + 1 + n) & 0x0F;
                                divider = over - n * t.period;
                        }
                        t.divider = divider & 0xFF;
                }
        }
       
        // Runs timer to present if it's not already
        static void runTimer( Timer t, int time )
        {
                if ( time >= t.time )
                        runTimer_( t, time );
        }
       
        // Enables/disables boot ROM by swapping it out of RAM
        private void enableRom( int enable )
        {
                if ( romEnabled != enable )
                {
                        romEnabled = enable;
                        if ( enable != 0 )
                        {
                                System.arraycopy( ram, romAddr, hiRam, 0, romSize );
                                System.arraycopy( rom, 0, ram, romAddr, romSize );
                        }
                        else
                        {
                                System.arraycopy( hiRam, 0, ram, romAddr, romSize );
                        }
                        // TODO: ROM can still get overwritten when DSP writes to echo buffer
                }
        }
       
        public void startTrack()
        {
                super.startTrack();
               
                time     = 0;
                dspTime = 32;
               
                // RAM
                java.util.Arrays.fill( ram, ramSize, ram.length, (byte) 0xFF );
                System.arraycopy( spc, ramOff, ram, 0, ramSize );
               
                dsp.init( ram, spc, dspStateOff );
               
                // CPU
                reset( ram );
                pc = (spc [cpuStateOff + 1] & 0xFF) << 8 | (spc [cpuStateOff] & 0xFF);
                a  = spc [cpuStateOff + 2] & 0xFF;
                x  = spc [cpuStateOff + 3] & 0xFF;
                y  = spc [cpuStateOff + 4] & 0xFF;
                sp = spc [cpuStateOff + 6] & 0xFF;
                setPsw( spc [cpuStateOff + 5] & 0xFF );
               
                // SMP registers
                for ( int i = 0; i < regCount; i++ )
                        regsIn [i] = regs [i] = ram [0xF0 + i] & 0xFF;
               
                regsIn [testReg    ] = 0; // these always read back as 0
                regsIn [controlReg ] = 0;
                regsIn [t0targetReg] = 0;
                regsIn [t1targetReg] = 0;
                regsIn [t2targetReg] = 0;
               
                // ROM
                romEnabled = 0;
                enableRom( regs [controlReg] & 0x80 );
               
                // Timers
                for ( int i = 0; i < timerCount; i++ )
                {
                        Timer t = timers [i] = new Timer();
                        t.time = 1;
                        t.divider = 0;
                        t.period  = ((regs [t0targetReg + i] - 1) & 0xFF) + 1;
                        t.enabled = regs [controlReg] >> i & 1;
                        t.counter = regsIn [t0outReg + i] & 0x0F;
                }
               
                timers [2].prescaler = 4;
                timers [1].prescaler = 4 + 3;
                timers [0].prescaler = 4 + 3;
               
                // Clear echo
                if ( (dsp.regs [dsp.r_flg] & 0x20) == 0 )
                {
                        int addr = (dsp.regs [dsp.r_esa] & 0xFF) << 8;
                        int end  = addr + ((dsp.regs [dsp.r_edl] & 0x0F) << 11);
                        if ( end > ramSize )
                                end = ramSize;
                        java.util.Arrays.fill( ram, addr, end, (byte) 0xFF );
                }
        }
       
        protected int play_( byte out [], int count )
        {
                dsp.setOutput( out );
               
                // Run for count/2*32 clocks + extra to get DSP time half-way between samples,
                // since CPU might run for slightly less than requested
                int clockCount = count * (32 / 2) + 16 - ((time - dspTime) & 31);
                time            -= clockCount;
                dspTime         -= clockCount;
                timers [0].time -= clockCount;
                timers [1].time -= clockCount;
                timers [2].time -= clockCount;
                runCpu();
               
                if ( time < 0 ) // emulation error
                {
                        logError();
                        return 0;
                }
               
                // Catch up to CPU
                runTimer( timers [0], time );
                runTimer( timers [1], time );
                runTimer( timers [2], time );
               
                // Run DSP to present
                int delta;
                if ( (delta = time - dspTime) >= 0 )
                {
                        delta = (delta >> 5) + 1;
                        dspTime += delta << 5;
                        dsp.run( delta );
                }
               
                assert dsp.sampleCount() == count;
                return dsp.sampleCount();
        }
       
        // Writes to SMP register
        private void writeReg( int addr, int data )
        {
                switch ( addr )
                {
                case t0targetReg:
                case t1targetReg:
                case t2targetReg: {
                        Timer t = timers [addr - t0targetReg];
                        int period = ((data - 1) & 0xFF) + 1;
                        if ( t.period != period )
                        {
                                runTimer( t, time );
                                t.period = period;
                        }
                        break;
                }
               
                case t0outReg:
                case t1outReg:
                case t2outReg:
                        // TODO
                        //if ( data < no_read_before_write / 2 )
                        //      run_timer( &m.timers [addr - t0outReg], time - 1 )->counter = 0;
                        break;
               
                // Registers that act like RAM
                case 0x8:
                case 0x9:
                        regsIn [addr] = data;
                        break;
               
                case testReg:
                        //if ( (uint8_t) data != 0x0A )
                        //      dprintf( "SPC wrote to test register\n" );
                        break;
               
                case controlReg:
                        // port clears
                        if ( (data & 0x10) != 0 )
                        {
                                regsIn [cpuio0Reg] = 0;
                                regsIn [cpuio1Reg] = 0;
                        }
                        if ( (data & 0x20) != 0 )
                        {
                                regsIn [cpuio2Reg] = 0;
                                regsIn [cpuio3Reg] = 0;
                        }
                       
                        // timers
                        for ( int i = 0; i < timerCount; i++ )
                        {
                                Timer t = timers [i];
                                int enabled = data >> i & 1;
                                if ( t.enabled != enabled )
                                {
                                        runTimer( t, time );
                                        t.enabled = enabled;
                                        if ( enabled != 0 )
                                        {
                                                t.divider = 0;
                                                t.counter = 0;
                                        }
                                }
                        }
                        enableRom( data & 0x80 );
                        break;
                }
        }
       
        public final void cpuWrite( int addr, int data )
        {
                // RAM
                ram [addr] = (byte) data;
                if ( (addr -= 0xF0) >= 0 ) // 64%
                {
                        // $F0-$FF
                        if ( addr < regCount ) // 87%
                        {
                                regs [addr] = (data &= 0xFF);
                               
                                // Ports
                               
                                // Registers other than $F2 and $F4-$F7
                                //if ( addr != 2 && addr != 4 && addr != 5 && addr != 6 && addr != 7 )
                                if ( (~0x2F000000 << addr) < 0 ) // 36%
                                {
                                        if ( addr == dspdataReg ) // 99%
                                        {
                                                // Run DSP to present
                                                int delta;
                                                if ( (delta = time - dspTime) >= 0 ) // 95%
                                                {
                                                        delta = (delta >> 5) + 1;
                                                        dspTime += delta << 5;
                                                        dsp.run( delta );
                                                }
                                               
                                                int dspaddr;
                                                if ( (dspaddr = regs [dspaddrReg]) <= 0x7F )
                                                        dsp.write( dspaddr, data );
                                        }
                                        else
                                        {
                                                writeReg( addr, data );
                                        }
                                }
                        }
                        // IPL ROM area or address wrapped around
                        else if ( (addr -= romAddr - 0xF0) >= 0 ) // 1% in IPL ROM area or address wrapped around
                        {
                                if ( addr < romSize )
                                {
                                        hiRam [addr] = (byte) data;
                                        if ( romEnabled != 0 )
                                                ram [addr + romAddr] = rom [addr]; // restore overwritten ROM
                                }
                                else
                                {
                                        ram [addr + romAddr] = (byte) 0xFF; // restore overwritten padding
                                        cpuWrite( data, addr - (ramSize - romAddr) );
                                }
                        }
                }
        }
       
        public final int cpuRead( int addr )
        {
                // Low RAM
                if ( addr < 0xF0 ) // 60%
                        return ram [addr] & 0xFF;
               
                // Timers
                if ( (addr ^= 0xFF) < timerCount ) // 68%
                {
                        Timer t = timers [2 - addr]; // TODO: reorder timers to eliminate 2-
                        if ( time >= t.time )
                                runTimer_( t, time );
                        int result = t.counter;
                        t.counter = 0;
                        return result;
                }
               
                // Other registers
                if ( (addr ^= 0xFF) <= 0xFF ) // 9%
                {
                        if ( addr == dspaddrReg + 0xF0 )
                                return regs [dspaddrReg];
                       
                        if ( addr == dspdataReg + 0xF0 )
                        {
                                // DSP
                               
                                // Run to present
                                int delta;
                                if ( (delta = time - dspTime) >= 0 ) // 1%
                                {
                                        delta = (delta >> 5) + 1;
                                        dspTime += delta << 5;
                                        dsp.run( delta );
                                }
                               
                                return dsp.regs [regs [dspaddrReg] & 0x7F] & 0xFF;
                        }
                       
                        return regsIn [addr - 0xF0];
                }
               
                // RAM
                if ( addr <= 0xFFFF ) // 99%
                        return ram [addr] & 0xFF;
               
                // Address wrapped around
                return cpuRead( addr - 0x10000 );
        }
}